/*
 * Licensed Materials - Property of tenxcloud.com
 * (C) Copyright 2018 TenxCloud. All Rights Reserved.
 * 2018-11-06  @author lizhen
 */

package middleware

import (
	"github.com/gin-gonic/gin"
	"github.com/golang/glog"
	"github.com/jinzhu/gorm"
	apierr "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"modules.tenxcloud.com/common/composite"
	"modules.tenxcloud.com/common/contract"
	"modules.tenxcloud.com/common/model/permission"
	"modules.tenxcloud.com/common/model/project"
	"modules.tenxcloud.com/common/utility/errors"
	"net/http"
)

const (
	ACLFilterKey = "acl-filter"
)

var (
	SystemAdminAndPlatformAdmin       = []int{SystemAdmin, PlatformAdmin}
	SystemAdminAndInfrastructureAdmin = []int{SystemAdmin, InfrastructureAdmin}
	AllAdmins                         = []int{SystemAdmin, PlatformAdmin, InfrastructureAdmin}
)

func CheckProjectAdmin(context *gin.Context) {
	checkProjectAdmin(context, []int{})
}

func CheckProjectAdminWithBypassRoles(bypassRoles []int) func(*gin.Context) {
	return func(context *gin.Context) {
		checkProjectAdmin(context, bypassRoles)
	}
}

func CheckPermissionWithBypassRoles(
	getClusterID func(*gin.Context) string,
	code string,
	bypassRoles []int) func(*gin.Context) {
	return func(context *gin.Context) {
		validate(code, getClusterID, bypassRoles, context)
	}
}

func CheckPermissionPredicateWithBypassRoles(
	getClusterID func(*gin.Context) string,
	mappings ...func(*gin.Context) (bool, string, []int)) func(*gin.Context) {
	return func(context *gin.Context) {
		for _, mapping := range mappings {
			if ok, code, bypassRoles := mapping(context); ok {
				validate(code, getClusterID, bypassRoles, context)
				break
			}
		}
	}
}

func CheckPermission(getClusterID func(*gin.Context) string, code string) func(*gin.Context) {
	return func(context *gin.Context) {
		validate(code, getClusterID, SystemAdminAndPlatformAdmin, context)
	}
}

func CheckPermissionPredicate(
	getClusterID func(*gin.Context) string,
	mappings ...func(*gin.Context) (bool, string)) func(*gin.Context) {
	return func(context *gin.Context) {
		for _, mapping := range mappings {
			if ok, code := mapping(context); ok {
				validate(code, getClusterID, SystemAdminAndPlatformAdmin, context)
				break
			}
		}
	}
}

func validate(permissionCode string, getClusterID func(*gin.Context) string, bypassRoles []int, context *gin.Context) {
	li, exist := context.Get(LoginUserKey)
	if !exist {
		glog.Fatal("CheckPermission middleware should work together with Authorization middleware")
	}
	u, _ := li.(contract.User)
	for _, role := range bypassRoles {
		if userRole := u.GetRole(); userRole == role {
			glog.V(4).Infof("bypass permission validation for admin user, user id %d, role %d", u.GetUserID(), userRole)
			context.Next()
			return
		}
	}
	ns, _ := context.Get(NamespaceKey)
	n, _ := ns.(*namespace)
	userID := u.GetUserID()
	projectName := n.space
	clusterID := getClusterID(context)
	pass, filter, err := validatePermission(userID, projectName, clusterID, permissionCode)
	if err != nil {
		glog.Warningf("validate permission failed, user id %d, project %s, cluster id %s, permission %s",
			userID, projectName, clusterID, permissionCode)
		context.AbortWithStatusJSON(errors.InternalServerError(err))
		return
	}
	if !pass {
		glog.Warningf("no permission granted, user id %d, project %s, cluster id %s, permission %s",
			userID, projectName, clusterID, permissionCode)
		context.AbortWithStatusJSON(errors.Forbidden(&apierr.StatusError{ErrStatus: metav1.Status{
			Status: metav1.StatusFailure,
			Code:   http.StatusForbidden,
			Reason: metav1.StatusReasonForbidden,
			Details: &metav1.StatusDetails{
				Kind: "ResourcePermission",
				Name: permissionCode,
			},
			Message: "user can not access resource due to acl",
		}}))
		return
	}
	if filter != nil {
		context.Set(ACLFilterKey, filter)
	}
	context.Next()
}

func validatePermission(userID int, projectName, clusterID, permissionCode string) (ok bool, filter []contract.ACL, err error) {
	var db *gorm.DB
	if db, err = composite.Database.GetOrm(); err != nil {
		glog.Errorf("get database connection failed, %s", err)
		return
	}
	var codes []string
	if codes, err = permission.PermissionsOfUserInProject(userID, projectName, db); err != nil {
		glog.Warningf("query permission failed, user id %d, project %s, cluster %s", userID, projectName, clusterID)
		return
	}
	if codes != nil && len(codes) > 0 {
		for _, code := range codes {
			if code == permissionCode || code == "SYSTEM_ALL_PRIVILEGES" {
				ok = true
				return
			}
		}
	}
	if filter, err = permission.ACLsOfUserInProjectInCluster(userID, projectName, clusterID, permissionCode, db); err != nil {
		glog.Warningf("query permission acl failed, user id %d, project %s, cluster %s", userID, projectName, clusterID)
		return
	}
	if filter == nil || len(filter) <= 0 {
		return
	}
	ok = true
	return
}

func isUserProjectAdmin(userID int, namespace string) (is bool, err error) {
	var db *gorm.DB
	if db, err = composite.Database.GetOrm(); err != nil {
		glog.Errorf("get database connection failed, %s", err)
		return
	}
	is, err = project.IsUserProjectAdmin(userID, namespace, db)
	return
}

func checkProjectAdmin(context *gin.Context, bypassRoles []int) {
	li, exist := context.Get(LoginUserKey)
	if !exist {
		glog.Fatal("CheckPermission middleware should work together with Authorization middleware")
	}
	u, _ := li.(contract.User)
	ns, _ := context.Get(NamespaceKey)
	n, _ := ns.(*namespace)
	if n.Type() != contract.ProjectSpace {
		glog.Warningf("check project admin failed, not in project space, %s", n.space)
		context.AbortWithStatusJSON(errors.Forbidden("action can only be operated on project space"))
		return
	}
	userID := u.GetUserID()
	for _, role := range bypassRoles {
		if userRole := u.GetRole(); userRole == role {
			glog.V(4).Infof("bypass permission validation for admin user, user id %d, role %d", u.GetUserID(), userRole)
			context.Next()
			return
		}
	}
	projectNamespace := n.space
	is, err := isUserProjectAdmin(userID, projectNamespace)
	if err != nil {
		glog.Warningf("check user project admin failed, user id %d, project %s, %s",
			userID, projectNamespace, err)
		context.AbortWithStatusJSON(errors.InternalServerError(err))
		return
	}
	if !is {
		glog.Warningf("check user project admin failed, not project admin, user id %d, project %s",
			userID, projectNamespace)
		context.AbortWithStatusJSON(errors.Forbidden("action can only be operated by project admin"))
		return
	}
	context.Next()
}
